/*!
 * scriptnum.js - script number object for bcoin.
 * Copyright (c) 2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

const assert = require('bsert');
const {I64} = require('n64');
const ScriptError = require('./scripterror');
const {inspectSymbol} = require('../utils');

/*
 * Constants
 */

const EMPTY_ARRAY = Buffer.alloc(0);

/**
 * Script Number
 * @see https://github.com/chjj/n64
 * @alias module:script.ScriptNum
 * @property {Number} hi
 * @property {Number} lo
 * @property {Number} sign
 */

class ScriptNum extends I64 {
  /**
   * Create a script number.
   * @constructor
   * @param {(Number|String|Buffer|Object)?} num
   * @param {(String|Number)?} base
   */

  constructor(num, base) {
    super(num, base);
  }

  /**
   * Cast to int32.
   * @returns {Number}
   */

  getInt() {
    if (this.lt(I64.INT32_MIN))
      return I64.LONG_MIN;

    if (this.gt(I64.INT32_MAX))
      return I64.LONG_MAX;

    return this.toInt();
  }

  /**
   * Serialize script number.
   * @returns {Buffer}
   */

  toRaw() {
    let num = this;

    // Zeroes are always empty arrays.
    if (num.isZero())
      return EMPTY_ARRAY;

    // Need to append sign bit.
    let neg = false;
    if (num.isNeg()) {
      num = num.neg();
      neg = true;
    }

    // Calculate size.
    const size = num.byteLength();

    let offset = 0;

    if (num.testn((size * 8) - 1))
      offset = 1;

    // Write number.
    const data = Buffer.allocUnsafe(size + offset);

    switch (size) {
      case 8:
        data[7] = (num.hi >>> 24) & 0xff;
      case 7:
        data[6] = (num.hi >> 16) & 0xff;
      case 6:
        data[5] = (num.hi >> 8) & 0xff;
      case 5:
        data[4] = num.hi & 0xff;
      case 4:
        data[3] = (num.lo >>> 24) & 0xff;
      case 3:
        data[2] = (num.lo >> 16) & 0xff;
      case 2:
        data[1] = (num.lo >> 8) & 0xff;
      case 1:
        data[0] = num.lo & 0xff;
    }

    // Append sign bit.
    if (data[size - 1] & 0x80) {
      assert(offset === 1);
      assert(data.length === size + offset);
      data[size] = neg ? 0x80 : 0;
    } else if (neg) {
      assert(offset === 0);
      assert(data.length === size);
      data[size - 1] |= 0x80;
    } else {
      assert(offset === 0);
      assert(data.length === size);
    }

    return data;
  }

  /**
   * Instantiate script number from serialized data.
   * @private
   * @param {Buffer} data
   * @returns {ScriptNum}
   */

  fromRaw(data) {
    assert(Buffer.isBuffer(data));

    // Empty arrays are always zero.
    if (data.length === 0)
      return this;

    // Read number (9 bytes max).
    switch (data.length) {
      case 8:
        this.hi |= data[7] << 24;
      case 7:
        this.hi |= data[6] << 16;
      case 6:
        this.hi |= data[5] << 8;
      case 5:
        this.hi |= data[4];
      case 4:
        this.lo |= data[3] << 24;
      case 3:
        this.lo |= data[2] << 16;
      case 2:
        this.lo |= data[1] << 8;
      case 1:
        this.lo |= data[0];
        break;
      default:
        for (let i = 0; i < data.length; i++)
          this.orb(i, data[i]);
        break;
    }

    // Remove high bit and flip sign.
    if (data[data.length - 1] & 0x80) {
      this.setn((data.length * 8) - 1, 0);
      this.ineg();
    }

    return this;
  }

  /**
   * Serialize script number.
   * @returns {Buffer}
   */

  encode() {
    return this.toRaw();
  }

  /**
   * Decode and verify script number.
   * @private
   * @param {Buffer} data
   * @param {Boolean?} minimal - Require minimal encoding.
   * @param {Number?} limit - Size limit.
   * @returns {ScriptNum}
   */

  decode(data, minimal, limit) {
    assert(Buffer.isBuffer(data));

    if (limit != null && data.length > limit)
      throw new ScriptError('UNKNOWN_ERROR', 'Script number overflow.');

    if (minimal && !ScriptNum.isMinimal(data))
      throw new ScriptError('UNKNOWN_ERROR', 'Non-minimal script number.');

    return this.fromRaw(data);
  }

  /**
   * Inspect script number.
   * @returns {String}
   */

  [inspectSymbol]() {
    return `<ScriptNum: ${this.toString(10)}>`;
  }

  /**
   * Test wether a serialized script
   * number is in its most minimal form.
   * @param {Buffer} data
   * @returns {Boolean}
   */

  static isMinimal(data) {
    assert(Buffer.isBuffer(data));

    if (data.length === 0)
      return true;

    if ((data[data.length - 1] & 0x7f) === 0) {
      if (data.length === 1)
        return false;

      if ((data[data.length - 2] & 0x80) === 0)
        return false;
    }

    return true;
  }

  /**
   * Decode and verify script number.
   * @param {Buffer} data
   * @param {Boolean?} minimal - Require minimal encoding.
   * @param {Number?} limit - Size limit.
   * @returns {ScriptNum}
   */

  static decode(data, minimal, limit) {
    return new this().decode(data, minimal, limit);
  }

  /**
   * Test whether object is a script number.
   * @param {Object} obj
   * @returns {Boolean}
   */

  static isScriptNum(obj) {
    return obj instanceof ScriptNum;
  }
}

/*
 * Expose
 */

module.exports = ScriptNum;
